package edu.del.springbootredisbusinesspractice.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.del.springbootredisbusinesspractice.pojo.Product;
import edu.del.springbootredisbusinesspractice.service.ProductService;
import edu.del.springbootredisbusinesspractice.mapper.ProductMapper;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.concurrent.TimeUnit;

/**
* @author 27917
* @description 针对表【product(商品详情表)】的数据库操作Service实现
* @createDate 2025-09-27 15:27:36
*/
@Service
@EnableScheduling
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product>
    implements ProductService{

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private TaskScheduler taskScheduler;

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Product getProductById(Integer id) {
        String cacheKey = "product:" + id;

        // 1. 先查缓存
        if (redisTemplate.hasKey(cacheKey)) {
            String productJson = redisTemplate.opsForValue().get(cacheKey);
            if ("NULL".equals(productJson)) {
                // 缓存中有特殊标记，表示数据库中无此数据
                return null;
            }
            try {
                return objectMapper.readValue(productJson, Product.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("缓存数据解析失败", e);
            }
        }

        // 2. 缓存没有，查数据库
        Product product = baseMapper.selectById(id);

        if (product != null) {
            // 3. 数据库有数据，写入缓存，设置24小时过期
            try {
                redisTemplate.opsForValue().set(cacheKey, objectMapper.writeValueAsString(product));
                // 防止 同一时间 多个key 时间过期 设置 随机 时间
                redisTemplate.expire(cacheKey, (long) (Math.random() * 60 * 60 * 24), TimeUnit.SECONDS);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("商品对象序列化失败", e);
            }
            return product;
        } else {
            // 4. 数据库也没有数据，缓存一个空标记，防止缓存穿透，设置较短过期时间
            try {
                redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
            } catch (Exception e) {
                // 即使空标记缓存失败，也不影响主流程
                System.err.println("缓存空对象失败：" + e.getMessage());
            }
            return null; // 返回 null 表示商品不存在
        }
    }

    /**
     * 方案1：缓存更新方案
     * 更新商品信息
     * 注意：在传统先更新数据库，再更新（写入）缓存这个方案是可行，但是在高并发场景下，可能会导致缓存数据不一致
     * ① 数据一致性问题：如果多个线程同时更新同一个商品，缓存更新顺序可能与数据库事务不一致，导致缓存与数据库数据版本不一致
     * ② 缓存覆盖风险：后更新的线程可能错误地覆盖了前一个线程的缓存，造成脏数据
     * ③ 缓存更新失败风险：如果Redis宕机/序列化失败/网络问题，缓存更新失败，会导致数据库是新数据，缓存是旧数据 -> 出现严重不一致
     * ④ 并发写竞争：多个请求同时更新，缓存容易被错误地多次覆盖
     *
     * @param product 商品信息
     * @return 更新结果
     *     @Override
     *     public int updateProduct(Product product) {
     *         // 获取 缓存的 key
     *         String cacheKey = "product:" + product.getId();
     *         // 1. 先更新数据库
     *         int updateResult = baseMapper.updateById(product);
     *         if (updateResult > 0) {
     *             redisTemplate.delete(cacheKey);
     *         }
     *         return 0;
     *     }
     */

    /**
     * 方案2: 延迟双删 => 最终一致性
     * CAP: 一致性、可用性、网络分区
     * @param product 商品信息
     * @return 更新结果
     */
    public int updateProduct(Product product) {
        String cacheKey = "product:" + product.getId();

        // 1. 删除缓存 key
        redisTemplate.delete(cacheKey);

        // 2. 更新数据库
        int updateResult = baseMapper.updateById(product);

        // 3. 异步延迟删除缓存
        taskScheduler.schedule(() -> {
            try {
                redisTemplate.delete(cacheKey);
            } catch (Exception e) {
                log.error("异步删除缓存失败：" + e.getMessage());
            }
        }, Instant.now().plusMillis(300));

        return updateResult; // 正确返回更新结果
    }
}




